﻿using Abp.Timing;
using FileHelpers;
using FluentFTP;
using Hangfire;
using Hicap.QRCodeWarehouse.Processor.Const;
using Hicap.QRCodeWarehouse.Processor.Extensions;
using Hicap.QRCodeWarehouse.Processor.Model;
using ICSharpCode.SharpZipLib.Zip;
using Microsoft.EntityFrameworkCore.Internal;
using MongoDB.Driver;
using MongoDB.Driver.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using FileInfo = Hicap.QRCodeWarehouse.Processor.Model.FileInfo;

namespace Hicap.QRCodeWarehouse.Processor.Manager
{
    /// <summary>
    /// Taken code repository
    /// </summary>
    public class CodeTakenManager : ManagerBase, ICodeTakenManager
    {
        private readonly FtpClient _ftpClient;
        private readonly IMongoClient _mongoClient;

        public CodeTakenManager(IMongoClient mongoClient, FtpClient ftpClient, IMongoClient mongoClient1) : base(mongoClient)
        {
            _ftpClient = ftpClient;
            _mongoClient = mongoClient1;
        }

        /// <summary>
        /// Taken codes as cursor
        /// </summary>
        /// <param name="config">CodeTakenConfig</param>
        /// <param name="collection">IMongoCollection&lt;QrCodeRecord&gt;</param>
        /// <param name="totalCount"> out long</param>
        /// <returns>IFindFluent&lt;QrCodeRecord, QrCodeRecord&gt;</returns>
        private IFindFluent<QrCodeRecord, QrCodeRecord> GetCodes(CodeTakenConfig config,
            IMongoCollection<QrCodeRecord> collection, out long totalCount)
        {
            //make query condition
            Expression<Func<QrCodeRecord, bool>> expression = x => !string.IsNullOrEmpty(x.Code);

            if (config.IsNotDownloaded)
            {
                expression = expression.And(x => x.IsDownloaded == false);
            }

            if (config.IsNotUsed)
            {
                expression = expression.And(x => x.IsUsed == false);
            }

            //get total count
            totalCount = collection.CountDocuments(expression);

            //when total Count less-than limit,load limit count records;
            if (totalCount > config.Limit) totalCount = config.Limit;

            //return cursor
            return collection.Find(expression).Limit(config.Limit);
        }

        /// <summary>
        /// Get totoal download count
        /// </summary>
        /// <param name="config">CodeTakenConfig</param>
        /// <returns></returns>
        public async Task<int> GetTotoalDownloadCount(CodeTakenConfig config)
        {
            IMongoCollection<QrCodeRecord> collection = base.GetCollection<QrCodeRecord>(string.Join('_', config.ProductCode, config.GenerationRuleType));

            return await collection.AsQueryable().CountAsync(x => x.IsDownloaded);
        }

        /// <summary>
        /// make data file for download
        /// </summary>
        /// <param name="config">CodeTakenConfig</param>
        /// <param name="progress">Action&lt;Decimal&gt;</param>
        /// <returns>string</returns>
        public async Task<FileInfo> GenDataFile(CodeTakenConfig config, Action<decimal> progress)
        {
            //collection
            IMongoCollection<QrCodeRecord> collection = base.GetCollection<QrCodeRecord>(string.Join('_', config.ProductCode, config.GenerationRuleType));

            //cursor
            IFindFluent<QrCodeRecord, QrCodeRecord> cursor = GetCodes(config, collection, out long totalCount);

            //Write File Engine
            FileHelperAsyncEngine<QrCodeRecord> engine = new FileHelperAsyncEngine<QrCodeRecord>();

            //path
            string relationDir = $"Download/{config.UserId}/{Clock.Now:yyyy-MM-dd-HH}";
            string returnPath = $"{relationDir}/{config.FileName}.zip";

            //for backgroud update
            List<string> codeList = new List<string>();

            string tempFilePath = Path.GetTempFileName();

            //create file
            using (FileStream fsOut = File.Create(tempFilePath))
            {
                //file output stream
                using (ZipOutputStream zipStream = new ZipOutputStream(fsOut))
                {
                    //set password
                    if (!string.IsNullOrEmpty(config.Password))
                        zipStream.Password = config.Password;

                    //put zip entry and set zip config
                    ZipEntry newEntry = new ZipEntry(config.FileName);
                    zipStream.PutNextEntry(newEntry);
                    zipStream.SetLevel(8); //0-9, 9 being the highest level of compression
                    zipStream.IsStreamOwner = true;

                    //write file
                    using (engine.BeginWriteStream(new StreamWriter(zipStream)))
                    {
                        //init
                        long i = 1;
                        decimal curProgress = 0;
                        progress?.Invoke(0);

                        await cursor.ForEachAsync(x =>
                        {
                            codeList.Add(x.Code);

                            //write
                            engine.WriteNext(x);

                            //current progress
                            curProgress = decimal.Round((decimal)i / totalCount, decimals: 3,
                            mode: MidpointRounding.ToEven);

                            //in pending
                            if (i % SysConst.TakenFileInterval == 0 && curProgress != 1)
                            {
                                progress?.Invoke(curProgress);
                                i++;
                            }
                        });

                        //complete
                        progress?.Invoke(1);
                    }
                }

                if (!_ftpClient.DirectoryExists(relationDir))
                {
                    _ftpClient.CreateDirectory(relationDir);
                }

                await _ftpClient.UploadAsync(File.Open(tempFilePath, FileMode.Open), returnPath);
            }

            //backgroud update record info
            BackgroundJob.Schedule<ICodeTakenManager>((x) => x.UpdateCodeStatus(codeList, config), TimeSpan.FromSeconds(5));

            return new FileInfo()
            {
                FilePath = returnPath,
                FileSize = (new System.IO.FileInfo(tempFilePath)).Length,
                FileRecoredCount = codeList.Count
            };
        }

        /// <summary>
        /// Update record info
        /// </summary>
        /// <param name="list">List&lt;string&gt;</param>
        /// <param name="config">CodeTakenConfig</param>
        public void UpdateCodeStatus(List<string> list, CodeTakenConfig config)
        {
            if (list == null || list.Count == 0) return;

            //collection
            IMongoCollection<QrCodeRecord> collection = base.GetCollection<QrCodeRecord>(string.Join('_', config.ProductCode, config.GenerationRuleType));

            //update setting
            UpdateDefinition<QrCodeRecord> update = Builders<QrCodeRecord>.Update
                .Set(x => x.IsDownloaded, true)
                .Set(x => x.DownloadedTime, Clock.Now.AddSeconds(-5));

            List<UpdateOneModel<QrCodeRecord>> bulkList = new List<UpdateOneModel<QrCodeRecord>>();

            //update in session ctrl
            //loop update
            foreach (string s in list)
            {
                UpdateOneModel<QrCodeRecord> updateModel = new UpdateOneModel<QrCodeRecord>(Builders<QrCodeRecord>.Filter.Eq(y => y.Code, s), update);
                bulkList.Add(updateModel);
            }

            using (IClientSessionHandle clientSession = _mongoClient.StartSession())
            {
                try
                {
                    clientSession.StartTransaction();
                    collection.BulkWrite(clientSession, bulkList);
                    clientSession.CommitTransaction();
                }
                catch
                {
                    clientSession.AbortTransaction();
                    throw;
                }
            }
        }
    }
}